<!--
 * @Description:
     基于vue3.x.x、vue-router4.x 版本的keepalive
     支持  🎉🎉🎉
        1、路由 判断 前进/后退状态
        2、路由 缓存
        3、路由 h5、pc 切换过渡动效
        4、路由iframe 缓存
     后期可以考虑放在npm包里
 * @Author bianpengfei
 * @create 2022/11/23 20:58
 -->
<script>
export default { name: 'GRouterViewCacheNext' }
</script>

<!--setup-->
<script setup>
import {
  computed,
  onMounted,
  onUnmounted,
  onBeforeMount,
  getCurrentInstance,
  nextTick,
  shallowReactive,
  shallowRef
} from 'vue'
import { arrayToObj, isFunction, isPlainObject, parseJsonNoError, uuid } from 'sf-utils2'
import { useRoute, useRouter } from 'vue-router'
import {
  generateNavAttr,
  proxyArrayProto,
  activateChildComponent,
  deactivateChildComponent,
  rmCacheRoute
} from './utils'

let _routeDirection = '' // 路由方向
let _routingStackHistory = parseJsonNoError(sessionStorage.ROUTING_STACK_HISTORY) || [] // 路由堆栈历史
let _maxRoutingStackHistoryLen = 500

const { proxy } = getCurrentInstance()

const route = useRoute()
const router = useRouter()

proxyArrayProto(_routingStackHistory, () => {
  if (_routingStackHistory.length > _maxRoutingStackHistoryLen) {
    // 堆栈历史路由，如果超过最大堆栈历史路由
    _routingStackHistory.splice(0, 1)
  }
  sessionStorage.ROUTING_STACK_HISTORY = JSON.stringify(_routingStackHistory)
})

/* 定义props */
const props = defineProps({
  /**
   *  stack:类似app那样回退（返回）自动销毁缓存vNode，适用场景主要用于h5
   *  flat：平铺化路由，不会去自动销毁缓存vNode，适用场景主要用于pc
   */
  cacheMode: {
    type: String,
    default: 'stack', // stack flat
    validator: (val) => ['stack', 'flat'].includes(val)
  },
  // 是否开启路由缓存
  pageCache: {
    type: Boolean,
    default: true
  },
  // 过度动画名称
  transitionName: {
    type: String,
    default: ''
  },
  // 是否开启路由过度动画
  transition: {
    type: Boolean,
    default: true
  },
  // 缓存的最大值
  max: {
    type: Number,
    default: 99
  },
  // 包括name
  include: {
    type: [Array, String],
    default: () => []
  },
  // 是否第一动画
  appear: {
    type: Boolean,
    default: true
  },
  // 包括name
  mode: {
    type: String,
    default: '' // out-in【当pc端时，设置out-in 配合transitionName="slide-transform"】
  },
  // 排除缓存name
  exclude: {
    type: [Array, String],
    default: () => []
  },
  // 自定义类名
  customClass: {
    type: String,
    default: ''
  }
})

/* 定义emits */
const emit = defineEmits(['route-change'])

/* 定义数据 */
const keepAliveRef = shallowRef(null)
const state = shallowReactive({
  componentWrapKey: uuid(),
  isReload: true,
  innerTransitionName: 'fade',
  routingStack: [], // 路由堆栈
  iframeComponents: [] // iframe组件
})

/* 方法区 */
/**
 * @description 重新加载
 * @param {Object} curRoute 当前路由实例
 * @expose 对外暴露
 * @return
 */
const reload = (curRoute = route) => {
  if (isCanCache(curRoute)) {
    // 处理是iframe的
    if (curRoute?.meta?.iframe === true) {
      const refName = `iframe${curRoute.fullPath.replace(/\//g, '')}Ref`
      if (proxy.$refs[refName]) {
        const [componentInstance] = proxy.$refs[refName]
        const reloadComponentFunc = componentInstance.$refs[`route@ref`]?.reload // 组件中是否存在reload方法
        if (isFunction(reloadComponentFunc)) {
          keepAliveRef.value.$ && removeCacheRoute(curRoute.fullPath)
          reloadComponentFunc()
        } else {
          // 不存在
          const idx = state.iframeComponents.findIndex((v) => v.fullPath === curRoute.fullPath)
          if (~idx) {
            state.iframeComponents.splice(idx, 1)
            setTimeout(() => {
              state.iframeComponents.push({
                fullPath: curRoute.fullPath,
                key: uuid(),
                component: curRoute.matched?.at?.(-1)?.components?.default
              })
              state.iframeComponents = [...state.iframeComponents]
            }, 16)
          }
        }
      }
    } else {
      // 普通路由
      keepAliveRef.value.$ && removeCacheRoute(curRoute.fullPath)
    }
  }

  state.isReload = false
  setTimeout(() => {
    nextTick(() => {
      state.isReload = true
    })
  }, 20)
}

/**
 * @description 添加路由堆栈
 * @param {Object} data 当前路由实例
 * @return
 */
const pushRoutingStack = (data) => {
  if (props.cacheMode === 'stack') {
    // 堆栈缓存模式
    const idx = state.routingStack.findIndex((v) => v.fullPath === data.fullPath)
    let routeStackNew = state.routingStack.slice(0)
    if (idx >= 0) {
      routeStackNew = state.routingStack.slice(0, idx + 1)
    } else {
      routeStackNew.push(data)
    }
    state.routingStack = routeStackNew
  } else {
    // 平铺缓存模式
    state.routingStack = [...state.routingStack, data]
  }
}

/**
 * 判断路由是否可以缓存
 */
const isCanCache = (route) => {
  return props.pageCache && route?.meta?.cache !== false
}

/**
 * 移除缓存组件
 * @param {string} fullPath
 */
const removeCacheRoute = (fullPath) => {
  keepAliveRef.value.$ && rmCacheRoute(fullPath, keepAliveRef.value.$)
}

/**
 * 移除iframe缓存组件
 * @param {string} fullPath
 */
const removeIframeRoute = (fullPath) => {
  state.iframeComponents = state.iframeComponents.filter((v) => v.fullPath != fullPath)
  keepAliveRef.value.$ && rmCacheRoute(fullPath, keepAliveRef.value.$)
}

/* 计算 */
/**
 * 是否是iframe缓存路由
 * @type {ComputedRef<unknown>}
 * @private
 */
const _isIframeCacheRoute = computed(() => {
  return isCanCache(route) && route.meta?.iframe === true
})

// iframe 组件映射
const _iframeComponentsMap = computed(() => arrayToObj(state.iframeComponents, 'fullPath'))

/**
 * 过渡动画名称
 * @type {ComputedRef<unknown>}
 * @private
 */
const _transitionName = computed(() => {
  if (props.transition) {
    return props.transitionName ? props.transitionName : state.innerTransitionName || ''
  }
  return ''
})

/**
 * 缓存路由数组
 * @type {ComputedRef<unknown>}
 * @private
 */
const _cacheRoutes = computed(() => {
  if (!props.pageCache) return []
  return [
    ...new Set([
      ...(router?.getRoutes?.() || [])
        .filter((v) => v?.components?.name && v?.meta?.permanentCache === true)
        .map((v) => v?.components?.name || v?.name),
      ...state.routingStack.reduce((pre, cur) => {
        if (Array.isArray(cur)) {
          cur.forEach((v) => {
            if (v.name && isCanCache(v)) pre.push(v.name)
          })
        } else if (isPlainObject(cur)) {
          if (cur.name && isCanCache(cur)) pre.push(cur.name)
        }
        return pre
      }, []),
      ...props.include
    ])
  ]
})

/* 监听 */
/**
 * 监听路由变化
 */
proxy.$watch(
  () => proxy.$route,
  (newVal, oldVal) => {
    // console.log('watch', newVal?.fullPath, oldVal?.fullPath)
    if (isCanCache(newVal) && newVal?.meta?.iframe === true) {
      let ctor = state.iframeComponents.find((v) => v.fullPath === route.fullPath)
      const refName = `iframe${newVal.fullPath.replace(/\//g, '')}Ref`
      if (!ctor) {
        // console.log('newVal', router.resolve(newVal), newVal.matched, newVal.matched.value)
        const isExist = state.iframeComponents.some((v) => v.fullPath == newVal.fullPath)
        if (!isExist) {
          const instance = newVal.matched.at(-1)?.components?.default
          state.iframeComponents.push({
            fullPath: newVal.fullPath,
            key: uuid(),
            component: instance
          })
          state.iframeComponents = [...state.iframeComponents]
          nextTick(() => {
            const [componentInstance] = [proxy.$refs[refName]].flat(Infinity)
            console.log('[proxy.$refs[refName]].flat(Infinity)', [proxy.$refs[refName]].flat(Infinity))
            if (componentInstance) {
              // const vnode = componentInstance.$.vnode
              // vnode.shapeFlag |= 256 // vue3 keepalive 源码
              componentInstance && activateChildComponent(componentInstance)
            }
          })
        }
      } else {
        const [componentInstance] = [proxy.$refs[refName]].flat(Infinity) || []
        componentInstance && activateChildComponent(componentInstance)
      }
    }

    if (isCanCache(oldVal) && oldVal?.meta?.iframe === true) {
      const refName = `iframe${oldVal.fullPath.replace(/\//g, '')}Ref`
      console.log('proxy', proxy.$refs, refName)
      const componentInstance = [proxy.$refs[refName]].flat(Infinity)
      componentInstance && deactivateChildComponent(componentInstance)
    }

    // 1、设置路由 前进还是后退
    if (newVal?.fullPath === '/' || !oldVal?.fullPath) {
      // 第一次进来
      _routeDirection = ''
      state.innerTransitionName = 'fade'
    } else {
      const isExist = state.routingStack.some((v) => v?.fullPath === newVal?.fullPath)
      if (isExist) {
        // 已存在
        _routeDirection = 'backward' // 后退
        state.innerTransitionName = 'pop-out' //  'pop-out'
      } else {
        // 不存在
        state.innerTransitionName = 'pop-in'
        _routeDirection = 'forward' // 前进
      }
    }
    const navAttr = generateNavAttr(newVal)
    _routingStackHistory.push(navAttr)
    pushRoutingStack(navAttr)
    emit('route-change', {
      routeDirection: _routeDirection, // 方向
      toFullPath: newVal.fullPath,
      fromFullPath: oldVal?.fullPath,
      route: newVal,
      routingStack: state.routingStack,
      routingStackHistory: _routingStackHistory
    })
  },
  { immediate: true }
)

proxy.$watch(
  () => state.routingStack,
  (newVal, oldVal) => {
    if (props.cacheMode === 'stack') {
      // 堆栈缓存模式
      if (newVal.length < oldVal.length) {
        const removeView = oldVal.find((o) => {
          return newVal.findIndex((i) => i.fullPath === o.fullPath) < 0
        })
        if (removeView && removeView.meta?.cache !== false) {
          nextTick(() => {
            removeCacheRoute(removeView.fullPath)
          })
        }
      }
    } else {
      // 平铺缓存模式
    }
    sessionStorage.ROUTING_STACK = JSON.stringify(newVal)
  }
)

/* 生命周期 */
onBeforeMount(() => {
  const localCacheMode = parseJsonNoError(sessionStorage.CACHE_MODE) || ''
  if (localCacheMode !== props.cacheMode) {
    sessionStorage.ROUTING_STACK_HISTORY = JSON.stringify([])
    sessionStorage.ROUTING_STACK = JSON.stringify([])
  }
  sessionStorage.CACHE_MODE = JSON.stringify(props.cacheMode)
})

onMounted(() => {})

onUnmounted(() => {})

/* 暴露对象 */
// @expose
defineExpose({
  $: proxy.$,
  keepAliveRef,
  state,
  reload, // 重新加载路由
  removeIframeRoute,
  removeCacheRoute // 删除缓存路由
})
</script>

<!--render-->
<template>
  <div :class="['router-view-cache-next', customClass]">
    <!-- keepalive   -->
    <div :class="[`router-view router-view-pain`, (!state.isReload || _isIframeCacheRoute) && 'router-view--hidden']">
      <div class="router-view__main">
        <router-view
          v-slot="{ Component }"
          ref="routerViewRef"
        >
          <transition
            :name="_transitionName"
            appear
            :mode="mode"
          >
            <keep-alive
              ref="keepAliveRef"
              :include="_cacheRoutes"
              :exclude="props.exclude"
              :max="props.max"
            >
              <component
                :is="Component"
                v-if="state.isReload && !_isIframeCacheRoute"
                :key="route.fullPath"
                class="w-full min-h-full"
              />
            </keep-alive>
          </transition>
        </router-view>
      </div>
    </div>

    <!-- iframe的  -->
    <transition-group
      :name="_transitionName"
      :mode="mode"
      :appear="appear"
      tag="div"
      :class="[`router-view router-view-iframe`, _iframeComponentsMap[route.fullPath] && `router-view-iframe--active`]"
    >
      <template
        v-for="item in state.iframeComponents"
        :key="item.fullPath"
      >
        <div
          :class="[
            `router-view__inner router-view-iframe__inner`,
            route.fullPath === item.fullPath && `router-view-iframe__inner--active`
          ]"
        >
          <component
            :is="item.component"
            :ref="`iframe${item.fullPath.replace(/\//g, '')}Ref`"
          />
        </div>
      </template>
    </transition-group>
  </div>
</template>

<!--style-->
<style scoped lang="less">
@import './transition.less';
.router-view-cache-next {
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  position: relative;
  //content-visibility: auto;
  .router-view--hidden {
    height: 0 !important;
    opacity: 0;
  }
  .router-view {
    width: inherit;
    height: inherit;
    position: relative;
    .router-view__main {
      width: 100%;
      height: 100%;
      overflow: auto;
      //overflow: hidden auto;
      //padding-right: 16px;
    }
    .router-view__inner {
      width: inherit;
      height: inherit;
      position: relative;
    }

    &-iframe {
      position: absolute;
      top: 0;
      left: 0;
      opacity: 0;
      z-index: -1;
      &--active {
        opacity: 1;
        z-index: 1;
      }
      .router-view-iframe__inner {
        position: absolute;
        width: 100%;
        height: 100%;
        z-index: -1;
        opacity: 0;
        top: 0;
        left: 0;
        &--active {
          opacity: 1;
          z-index: 1;
        }
      }
    }
  }
}
</style>
